#include "common.h"
#include "gputracer.h"
#include "glwindow.h"
#include "stb_image.h"


// PBO macro (see spec for details)
#define BUFFER_OFFSET(i) ((char *)NULL + (i))

GLuint readTex = 0;
GLuint writeTex = 1;
GLenum attachmentpoints[] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT };

GPUTracer::GPUTracer(int w, int h) : iWidth(w), iHeight(h)
{
	pData = NULL;
}

GPUTracer::~GPUTracer()
{
	if (pData != NULL)
		delete[] pData;
}

GLuint fb;

//initialization of shaders etc
void GPUTracer::init()
{
	srand( (unsigned)time( NULL ) );
	//create FBO

	glGenFramebuffersEXT(1,&fb); 
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,fb);


	//create common textures
	glGenTextures(2, &iTexture[0]);
	for (int i = 0; i < 2; i++){
		glBindTexture(GL_TEXTURE_2D, iTexture[i]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16, iWidth, iHeight, 0, GL_RGBA, GL_FLOAT, 0);
	}

	glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attachmentpoints[writeTex], GL_TEXTURE_2D, iTexture[writeTex], 0);
	glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, attachmentpoints[readTex], GL_TEXTURE_2D, iTexture[readTex], 0);

	glBindTexture(GL_TEXTURE_2D, iTexture[0]);

	//create and link intersection shader

	if (!pIsec.Create(szShaderFamily))
	{
		exit(1);
	}

	if (!pShading.Create(szShaderFamily+std::string("_shading")))
	{
		exit(1);
	}

	//setup uniform variables 

	//intersection
	pRay.Create(pIsec.pFragmentShader, "direction");
	pRayInv.Create(pIsec.pFragmentShader, "directionInv");
	pOpacitySwitch.Create(pIsec.pFragmentShader, "isOpacity");
	pRayTime.Create(pIsec.pFragmentShader, "dtime");
	iTex1ID = glGetUniformLocationARB(pIsec.pProgram, "texUnit");

	//shading
	pRayShade.Create(pShading.pFragmentShader, "direction");
	pLightSwitch.Create(pShading.pFragmentShader, "isLight");
	pShadowsSwitch.Create(pShading.pFragmentShader, "isShadows");
	iTex2ID = glGetUniformLocationARB(pShading.pProgram, "texUnit");

	//set camera to default position (a slight hack with zoom here)
	nFrame = 0;
	nTimeBase = 0;
	memset(rotMatrix, 0, 16*sizeof(float));
	rotMatrix[0] = rotMatrix[5] = rotMatrix[10] = rotMatrix[15] = 1;
	memcpy(rotMatrixInv, rotMatrix, 16*sizeof(float));
	fRotX = 0.0f; fRotY = 0.0f;
	zoom(170);

	//setup data (normalized data for points position)
	pData = new float[4*iWidth*iHeight];
	for (int i = 0 ; i < iWidth; i++)
	{
		for (int j = 0; j < iHeight; j++)
		{
			pData[(j + i*iHeight)*4 + 0] = (float)(j)/(float)iHeight;
			pData[(j + i*iHeight)*4 + 1] = (float)(i)/(float)iWidth;
			pData[(j + i*iHeight)*4 + 2] = 0;
			pData[(j + i*iHeight)*4 + 3] = 1;
		}
	}
}

//we call this function on each frame
void GPUTracer::update()
{
	if (!pData)
		return;

	//set viewport to 2^n square, set orthogonal projection and -1...1 bounding box
	int vp[4];
	glGetIntegerv(GL_VIEWPORT, vp);
	
	glViewport(0, 0, iWidth, iHeight);

	//clear screen and z-buffer
	glClearColor(1,1,1,1);
	glClear(GL_COLOR_BUFFER_BIT);
	glClear(GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_ALWAYS);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	float fTime = (float)(glutGet(GLUT_ELAPSED_TIME))/5000;

	//prepare data for tracer (ray start points)
	float * fInputData;
	fInputData = new float[4*iWidth*iHeight];
	memcpy(fInputData, pData, 4*iWidth*iHeight*sizeof(float));
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,fb);
	glDrawBuffer (attachmentpoints[writeTex]);

	//copy data to texture
	glBindTexture(GL_TEXTURE_2D, iTexture[readTex]);
//	glTexSubImage2D(GL_TEXTURE_2D, 0,0,0, iWidth, iHeight, GL_RGBA, GL_FLOAT, fInputData);

	//first pass: intersection
	pIsec.On();

	//setup uniform variables for shader
	glUniform1iARB(iTex1ID, 0);
	pRay.UniformMatrix4(rotMatrix);
	pOpacitySwitch.UniformInt(0);
	pRayTime.UniformFloat(sin(fTime)/2 + 0.5f);

	// draw quad 
	// intersection shader works and we have texture with intersection points
	renderQuad(1);
	pIsec.Off();
	
	glReadBuffer(attachmentpoints[writeTex]);
	glReadPixels(0, 0, iWidth, iHeight, GL_RGBA, GL_FLOAT, fInputData);
	memcpy(pData, fInputData, 4*iWidth*iHeight*sizeof(float));

	//second pass: shading
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,0);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glBindTexture(GL_TEXTURE_2D, iTexture[readTex]);
	glTexSubImage2D(GL_TEXTURE_2D, 0,0,0, iWidth, iHeight, GL_RGBA, GL_FLOAT, pData);
	pShading.On();
	glUniform1iARB(iTex2ID, 0);
	pRayShade.UniformMatrix4(rotMatrix);
	pLightSwitch.UniformInt(1);
	pShadowsSwitch.UniformInt(1);
	renderQuad(1);
	pShading.Off();

	fpsCounter();
	delete[] fInputData;
}

void GPUTracer::copyTexture(int nIndex)
{
	glBindTexture(GL_TEXTURE_2D, iTexture[nIndex]);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, iWidth, iHeight);
}

void GPUTracer::renderQuad(int nSubdivision, int nTextures, float z)
{
	assert (nSubdivision > 0);
	assert (nTextures == 1 || nTextures == 2);
	float fSubdiv = (float)nSubdivision; 
	glBegin(GL_QUADS);
	{
		for (int i = 0; i < nSubdivision; i++)
		{
			for (int j = 0; j < nSubdivision; j++)
			{
				float xmin = -1.0f + 2.0f*(float)i/fSubdiv;
				float xmax = -1.0f + 2.0f*(float)(i+1)/fSubdiv;
				float ymin = -1.0f + 2.0f*(float)j/fSubdiv;
				float ymax = -1.0f + 2.0f*(float)(j+1)/fSubdiv;

				float umin = (float)i/fSubdiv;
				float umax = (float)(i+1)/fSubdiv;
				float vmin = (float)j/fSubdiv;
				float vmax = (float)(j+1)/fSubdiv;

				if (nTextures == 1)
				{
					glTexCoord2f(umin, vmin); 
				}
				else
				{
					glMultiTexCoord2fARB(GL_TEXTURE0_ARB, umin, vmin);
//					glMultiTexCoord2fARB(GL_TEXTURE1_ARB, umin, vmin);
				}
				glVertex3f(xmin, ymin, z);
				if (nTextures == 1)
				{
					glTexCoord2f(umax, vmin); 
				}
				else
				{
					glMultiTexCoord2fARB(GL_TEXTURE0_ARB, umax, vmin);
//					glMultiTexCoord2fARB(GL_TEXTURE1_ARB, umax, vmin);
				}
				glVertex3f( xmax, ymin, z);
				if (nTextures == 1)
				{
					glTexCoord2f(umax, vmax); 
				}
				else
				{
					glMultiTexCoord2fARB(GL_TEXTURE0_ARB, umax, vmax);
//					glMultiTexCoord2fARB(GL_TEXTURE1_ARB, umax, vmax);
				}
				glVertex3f( xmax,  ymax, z);
				if (nTextures == 1)
				{
					glTexCoord2f(umin, vmax); 
				}
				else
				{
					glMultiTexCoord2fARB(GL_TEXTURE0_ARB, umin, vmax);
//					glMultiTexCoord2fARB(GL_TEXTURE1_ARB, umin, vmax);
				}
				glVertex3f(xmin,  ymax, z);
			}
		}
	}
	glEnd();
}

void GPUTracer::rotateX(float angle)
{
	fRotX += angle;
	if (fRotX > 360) fRotX -= 360;
	if (fRotX < 0) fRotX += 360;
	glPushMatrix();
	glLoadIdentity();
	glRotatef(angle, 1, 0, 0);
	glMultMatrixf(rotMatrix);
	glGetFloatv(GL_MODELVIEW_MATRIX, rotMatrix);
	glLoadIdentity();
	glRotatef(angle, -1, 0, 0);
	glMultMatrixf(rotMatrixInv);
	glGetFloatv(GL_MODELVIEW_MATRIX, rotMatrixInv);
	glPopMatrix();
}

void GPUTracer::rotateY(float angle)
{
	fRotY += angle;
	if (fRotY > 360) fRotY -= 360;
	if (fRotY < 0) fRotY += 360;
	glPushMatrix();
	glLoadIdentity();
	glRotatef(angle, 0, 1, 0);
	glMultMatrixf(rotMatrix);
	glGetFloatv(GL_MODELVIEW_MATRIX, rotMatrix);
	glLoadIdentity();
	glRotatef(angle, 0, -1, 0);
	glMultMatrixf(rotMatrixInv);
	glGetFloatv(GL_MODELVIEW_MATRIX, rotMatrixInv);
	glPopMatrix();
}

void GPUTracer::zoom(float delta)
{
	double s = exp((double)delta*0.01);
	glPushMatrix();
	glLoadIdentity();
	glScaled(s,s,s);
	glMultMatrixf(rotMatrix);
	glGetFloatv(GL_MODELVIEW_MATRIX, rotMatrix);
	glLoadIdentity();
	glScaled(1/s,1/s,1/s);
	glMultMatrixf(rotMatrixInv);
	glGetFloatv(GL_MODELVIEW_MATRIX, rotMatrixInv);
	glPopMatrix();
}

void GPUTracer::fpsCounter()
{
	nFrame++;
	nTime = glutGet(GLUT_ELAPSED_TIME);

	if (nTime - nTimeBase > 1000)
	{
		float fFps = (float)nFrame*1000.0f/(float)(nTime-nTimeBase);
		nTimeBase = nTime;
		nFrame = 0;
		char buffer[256];
		sprintf(buffer, "FRep Ray Tracer, fps: %f", fFps);
		glutSetWindowTitle(buffer);
	}
}
